Вы работаете в стартапе, который продаёт продукты питания. Нужно разобраться, как ведут себя пользователи вашего мобильного приложения.
Изучите воронку продаж. Узнайте, как пользователи доходят до покупки. Сколько пользователей доходит до покупки, а сколько — «застревает» на предыдущих шагах? На каких именно?
После этого исследуйте результаты A/A/B-эксперимента. Дизайнеры захотели поменять шрифты во всём приложении, а менеджеры испугались, что пользователям будет непривычно. Договорились принять решение по результатам A/A/B-теста. Пользователей разбили на 3 группы: 2 контрольные со старыми шрифтами (246, 247) и одну экспериментальную (248) — с новыми. Выясните, какой шрифт лучше.
import pandas as pd
import numpy as np
import scipy.stats as stats
import math as mth
from datetime import datetime, timedelta
from matplotlib import pyplot as plt
from plotly import graph_objects as go
events_df = pd.read_csv('/datasets/logs_exp.csv', sep='\t')
display(events_df.head())
display(events_df.info())
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
None
Таблица выгружена корректно, содержит 4 столбца и 244126 строк. Пропусков в данных нет.
events_df.columns = ['event_name', 'user_id', 'event_ts', 'group']
display(events_df.head())
| event_name | user_id | event_ts | group | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
В общей информации по таблице в разделе 1 видно, что пропусков в данных нет. Проверим на дубликаты.
display('Количество дубликатов', events_df.duplicated().sum())
'Количество дубликатов'
413
В сравнении с общим количеством строк дубликатов немного, их удаление не повлияет на качество данных.
events_df = events_df.drop_duplicates().reset_index(drop=True)
display('Количество дубликатов', events_df.duplicated().sum())
'Количество дубликатов'
0
Проверим набор значений в столбцах "event_name" и "group".
display(events_df['event_name'].unique())
display(events_df['group'].unique())
array(['MainScreenAppear', 'PaymentScreenSuccessful', 'CartScreenAppear',
'OffersScreenAppear', 'Tutorial'], dtype=object)
array([246, 248, 247])
В столбце "event_name" скрытых дубликатов нет. Значения по столбцу "group" соответствуют группам эксперимента.
Судя по всему, значения в столбце "event_ts" содержат время в формате unix. Учитывая это, не вижу необходимости в изменении форматов в исходной таблице. Добавим необходимые столбцы: даты и времени в формате datetime и отдельно даты.
events_df['event_dt'] = pd.to_datetime(events_df['event_ts'], unit= 's')
events_df['event_date'] = events_df['event_dt'].dt.date
display(events_df.head())
display(events_df.info())
| event_name | user_id | event_ts | group | event_dt | event_date | |
|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 | 2019-07-25 04:43:36 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 | 2019-07-25 11:11:42 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 | 2019-07-25 11:48:42 | 2019-07-25 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 243713 entries, 0 to 243712 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 243713 non-null object 1 user_id 243713 non-null int64 2 event_ts 243713 non-null int64 3 group 243713 non-null int64 4 event_dt 243713 non-null datetime64[ns] 5 event_date 243713 non-null object dtypes: datetime64[ns](1), int64(3), object(2) memory usage: 11.2+ MB
None
print('Количество событий', events_df['event_name'].count())
print('Количество пользователей', events_df['user_id'].nunique())
print('Среднее количество событий на одного пользователя', round(events_df['event_name'].count()/events_df['user_id'].nunique()))
Количество событий 243713 Количество пользователей 7551 Среднее количество событий на одного пользователя 32
print('Самая ранняя дата события', events_df['event_date'].min())
print('Самая поздняя дата события', events_df['event_date'].max())
events_df['event_dt'].hist(bins=14, figsize=(15, 5))
plt.title('Распределение событий по датам')
plt.xlabel("Дата")
plt.ylabel("Количество событий")
plt.show()
Самая ранняя дата события 2019-07-25 Самая поздняя дата события 2019-08-07
Таблица содержит данные о событиях за 2 недели, но количество событий распределено неравномерно. Полные данные есть только за одну неделю - с 01.08.2019 по 07.08.2019. Отбросим более ранние записи и проверим, как изменились данные.
events_df_actual = events_df.query('event_dt >= "2019-08-01"')
print('Количество событий до корректировки', events_df['event_name'].count())
print('Количество событий после корректировки', events_df_actual['event_name'].count())
print('Разница в количестве событий', events_df['event_name'].count() - events_df_actual['event_name'].count())
print('======================================================================================================')
print('Количество пользователей до корректировки', events_df['user_id'].nunique())
print('Количество пользователей после корректировки', events_df_actual['user_id'].nunique())
print('Разница в количестве пользователей', events_df['user_id'].nunique() - events_df_actual['user_id'].nunique())
print('======================================================================================================')
print('Количество действий на одного пользователя до корректировки', round(events_df['event_name'].count()/events_df['user_id'].nunique()))
print('Количество действий на одного пользователя после корректировки', round(events_df_actual['event_name'].count()/events_df_actual['user_id'].nunique()))
print('======================================================================================================')
print('Самая ранняя дата события', events_df_actual['event_date'].min())
print('Самая поздняя дата события', events_df_actual['event_date'].max())
Количество событий до корректировки 243713 Количество событий после корректировки 240887 Разница в количестве событий 2826 ====================================================================================================== Количество пользователей до корректировки 7551 Количество пользователей после корректировки 7534 Разница в количестве пользователей 17 ====================================================================================================== Количество действий на одного пользователя до корректировки 32 Количество действий на одного пользователя после корректировки 32 ====================================================================================================== Самая ранняя дата события 2019-08-01 Самая поздняя дата события 2019-08-07
Было отсеяно около 1% записей о событиях, что можно считать приемлемым, такое изменение не повлияет на качество данных. Полностью исключена информация о событиях только 17 пользователей. Среднее количество действий на пользователя не поменялось. Теперь таблица содержит данные за неделю наблюдений.
Проверим, что в таблицу попала информация о пользователях всех трёх экспериментальных групп.
display(events_df['group'].unique())
array([246, 248, 247])
В таблице остались данные пользователей из всех трех групп.
events_df_actual.groupby('event_name').agg({'user_id': 'count'}).sort_values(by = 'user_id', ascending=False)
| user_id | |
|---|---|
| event_name | |
| MainScreenAppear | 117328 |
| OffersScreenAppear | 46333 |
| CartScreenAppear | 42303 |
| PaymentScreenSuccessful | 33918 |
| Tutorial | 1005 |
Всего пять видов событий: открытие главного экрана, открытие экрана с предложениями товаров, открытие корзины, появление экрана успешной оплаты и прохождение обучения.
Больше всего раз пользователи открыти главный экран приложения - это событие занимает почти половину всех событий за исследуемый период. Намного меньше раз пользователи перешли на экран с предложениями товаров, похоже довольно часто пользователи просто заходят в приложение без намерения совершить покупку. На третьем месте по частоте событие перехода в корзину для просмотра и оплаты покупок, количество его воспроизведений немного меньше, чем у второго места. Почти на четверть меньше количество воспроизведений появления экрана об успешной оплате. На последнем месте - прохождение обучения, по сравнению с остальными событиями его воспроизвели ничтожное количество раз.
Посмторим на количество пользователей по событиям.
event_users = events_df_actual.groupby('event_name').agg({'user_id': 'nunique'}).sort_values(by = 'user_id', ascending=False)
event_users['part'] = round(event_users['user_id'] / events_df_actual['user_id'].nunique()*100, 2)
event_users
| user_id | part | |
|---|---|---|
| event_name | ||
| MainScreenAppear | 7419 | 98.47 |
| OffersScreenAppear | 4593 | 60.96 |
| CartScreenAppear | 3734 | 49.56 |
| PaymentScreenSuccessful | 3539 | 46.97 |
| Tutorial | 840 | 11.15 |
Практически все пользователи хоть раз перешли на главный экран приложения. На втором месте событие перехода на экран с предложенниями товаров - 61 процент пользователей на него перешли. На 10 процентов меньше пользователей хоть раз открыли корзину, и почти столько же увидели экран об успешной оплате. Только кажды 10 пользователь просмотрел обучение по использованию приложения, это действия явно не является обязательным.
С учётом описанного выше предположу, что корректная последовательность событий при использовании проложения такая:
Открытие главного экрана → Переход на экран с предложениями товаров → Переход в корзину → Успешная оплата товаров
Событие прохождения обучения предлагаю исключить из дальнейшего рассмотрения.
event_users = event_users.query('event_name != "Tutorial"')
pd.options.mode.chained_assignment = None
for i in range(len(event_users['part'])):
if i == 0:
event_users['part'][i] = 100
else:
event_users['part'][i] = round(event_users['user_id'][i] / event_users['user_id'][i-1] *100, 2)
display(go.Figure(go.Funnel(x = event_users['user_id'], y = event_users.index)))
display(event_users)
print('Процент пользователей, успешно оплативших заказ: ', round(event_users['user_id'][3] / event_users['user_id'][0] *100, 2))
| user_id | part | |
|---|---|---|
| event_name | ||
| MainScreenAppear | 7419 | 100.00 |
| OffersScreenAppear | 4593 | 61.91 |
| CartScreenAppear | 3734 | 81.30 |
| PaymentScreenSuccessful | 3539 | 94.78 |
Процент пользователей, успешно оплативших заказ: 47.7
До перехода на экран с предложениями товаров доходит только 60 процентов пользователей, и на этом моменте теряется большинство пользователей. Судя по всему, переход непосредственно к покупке товаров не является очевидным или удобным. Возможно, следует сделать обучение использованию приложения обязательным этапом при его первом открытии или переработать дизайн главной страницы.
После этого все выглядит лучше - 80 процентов пользователей, увидевших предложения товаров, перешли в корзину. Каждого пятого пользователя либо не устроило товарное предложение, либо он столкнулся с проблемой перехода в корзину.
После перехода в корзину у большинства пользователей не возникло проблем с оплатой товаров. Только 5 процентов не оплатило товары. Возможно, их не устроили условия доставки товаров или их постигли проблемы технического характера при оплате онлайн.
В целом, каждый второй пользователь, зашедший в приложение, в итоге совершил покупку. В целом, как мне кажется, это неплохо, но результат мог быть лучше, необходимо обратить внимание на переход с главной страницы приложения к странице товарных предложений.
events_df_actual.groupby('group').agg({'user_id': 'nunique'})
| user_id | |
|---|---|
| group | |
| 246 | 2484 |
| 247 | 2513 |
| 248 | 2537 |
Распределение пользователей по группам практически равное, сумма пользователей по группам соответствует общему количеству уникальных пользователей, значит пользователей, входящих сразу в несколько групп нет.
Также посчитаем количество пользователей, совершивших цепочку событий по каждой группе.
events_wo_tutorial = events_df_actual.query('event_name != "Tutorial"')
events_wo_tutorial.pivot_table(index = 'event_name', columns = 'group', values = 'user_id', aggfunc = 'nunique').sort_values(by=246, ascending=False)
| group | 246 | 247 | 248 |
|---|---|---|---|
| event_name | |||
| MainScreenAppear | 2450 | 2476 | 2493 |
| OffersScreenAppear | 1542 | 1520 | 1531 |
| CartScreenAppear | 1266 | 1238 | 1230 |
| PaymentScreenSuccessful | 1200 | 1158 | 1181 |
Составим функцию для расчета p_value.
Сразу примем уровень значимости равным 0,05, но стоит учесть, что при сравнении групп мы будем проводить сравнение 16 раз - для каждого события в цепочке умноженное на количество сравниваемых групп (две контрольные, объединенная из двух контрольных и целевая), чтобы снизить вероятность ошибки, предлагаю скорректировать его по методу Шидака.
alpha = 1 - (1 - 0.05) ** (1 / 16)
distr = stats.norm(0, 1)
def calc_prop_eq(group1, group2, event):
t1 = events_df_actual.query('group == @group1')['user_id'].nunique()
t2 = events_df_actual.query('group == @group2')['user_id'].nunique()
s1 = events_df_actual.query('group == @group1 and event_name == @event')['user_id'].nunique()
s2 = events_df_actual.query('group == @group2 and event_name == @event')['user_id'].nunique()
p1 = s1 / t1
p2 = s2 / t2
p_comb = (s1 + s2) / (t1 + t2)
z_value = (p1 - p2) / mth.sqrt(p_comb * (1 - p_comb) * (1/t1 + 1/t2))
p_value = (1 - distr.cdf(abs(z_value))) * 2
return p_value, p1, p2
Приступим к сравнению двух контрольных групп: 246 и 247. Нулевая гипотеза для проверки - доли пользователей, совершивших определённое действие для групп 246 и 247 не различается.
for i in events_wo_tutorial['event_name'].unique():
event_p_val = calc_prop_eq(246, 247, i)
print('Доля пользователей, совершивших событие {0} для группы 246: {1}'.format(i, round(event_p_val[1], 2)))
print('Доля пользователей, совершивших событие {0} для группы 247: {1}'.format(i, round(event_p_val[2], 2)))
print('p-значение по событию {0} для групп 246 и 247: {1}'.format(i, round(event_p_val[0], 2)))
if (event_p_val[0] < alpha):
print("Отвергаем нулевую гипотезу: между долями есть значимая разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
print('======================================================================================================')
Доля пользователей, совершивших событие MainScreenAppear для группы 246: 0.99 Доля пользователей, совершивших событие MainScreenAppear для группы 247: 0.99 p-значение по событию MainScreenAppear для групп 246 и 247: 0.76 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие OffersScreenAppear для группы 246: 0.62 Доля пользователей, совершивших событие OffersScreenAppear для группы 247: 0.6 p-значение по событию OffersScreenAppear для групп 246 и 247: 0.25 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие CartScreenAppear для группы 246: 0.51 Доля пользователей, совершивших событие CartScreenAppear для группы 247: 0.49 p-значение по событию CartScreenAppear для групп 246 и 247: 0.23 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие PaymentScreenSuccessful для группы 246: 0.48 Доля пользователей, совершивших событие PaymentScreenSuccessful для группы 247: 0.46 p-значение по событию PaymentScreenSuccessful для групп 246 и 247: 0.11 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ======================================================================================================
Ни в одном из 4 случаев мы не отвергаем нулевую гипотезу. Можем считать, что контрольные группы прошли проверку, можно приступить к проверке группы с измененными шрифтами.
Сначала проверим различия между группами 246 и 248. Нулевая гипотеза для проверки та же - доли пользователей, совершивших определённое действие в разных группах не различается.
for i in events_wo_tutorial['event_name'].unique():
event_p_val = calc_prop_eq(246, 248, i)
print('Доля пользователей, совершивших событие {0} для группы 246: {1}'.format(i, round(event_p_val[1], 2)))
print('Доля пользователей, совершивших событие {0} для группы 248: {1}'.format(i, round(event_p_val[2], 2)))
print('p-значение по событию {0} для групп 246 и 248: {1}'.format(i, round(event_p_val[0], 2)))
if (event_p_val[0] < alpha):
print("Отвергаем нулевую гипотезу: между долями есть значимая разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
print('======================================================================================================')
Доля пользователей, совершивших событие MainScreenAppear для группы 246: 0.99 Доля пользователей, совершивших событие MainScreenAppear для группы 248: 0.98 p-значение по событию MainScreenAppear для групп 246 и 248: 0.29 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие OffersScreenAppear для группы 246: 0.62 Доля пользователей, совершивших событие OffersScreenAppear для группы 248: 0.6 p-значение по событию OffersScreenAppear для групп 246 и 248: 0.21 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие CartScreenAppear для группы 246: 0.51 Доля пользователей, совершивших событие CartScreenAppear для группы 248: 0.48 p-значение по событию CartScreenAppear для групп 246 и 248: 0.08 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие PaymentScreenSuccessful для группы 246: 0.48 Доля пользователей, совершивших событие PaymentScreenSuccessful для группы 248: 0.47 p-значение по событию PaymentScreenSuccessful для групп 246 и 248: 0.21 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ======================================================================================================
По всем 4 проверкам мы не отвергаем нулевую гипотезу.
Очередь сравнения групп 247 и 248. Нулевая гипотеза остается неизменной - доли пользователей, совершивших определённое действие в разных группах не различается.
for i in events_wo_tutorial['event_name'].unique():
event_p_val = calc_prop_eq(247, 248, i)
print('Доля пользователей, совершивших событие {0} для группы 247: {1}'.format(i, round(event_p_val[1], 2)))
print('Доля пользователей, совершивших событие {0} для группы 248: {1}'.format(i, round(event_p_val[2], 2)))
print('p-значение по событию {0} для групп 247 и 248: {1}'.format(i, round(event_p_val[0], 2)))
if (event_p_val[0] < alpha):
print("Отвергаем нулевую гипотезу: между долями есть значимая разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
print('======================================================================================================')
Доля пользователей, совершивших событие MainScreenAppear для группы 247: 0.99 Доля пользователей, совершивших событие MainScreenAppear для группы 248: 0.98 p-значение по событию MainScreenAppear для групп 247 и 248: 0.46 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие OffersScreenAppear для группы 247: 0.6 Доля пользователей, совершивших событие OffersScreenAppear для группы 248: 0.6 p-значение по событию OffersScreenAppear для групп 247 и 248: 0.92 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие CartScreenAppear для группы 247: 0.49 Доля пользователей, совершивших событие CartScreenAppear для группы 248: 0.48 p-значение по событию CartScreenAppear для групп 247 и 248: 0.58 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие PaymentScreenSuccessful для группы 247: 0.46 Доля пользователей, совершивших событие PaymentScreenSuccessful для группы 248: 0.47 p-значение по событию PaymentScreenSuccessful для групп 247 и 248: 0.74 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ======================================================================================================
Во всех случаях у нас нет оснований считать, что показатели целевой группы отличаются от показателей контрольных групп.
Потивопоставим целевую группу объединенным контрольным. Нулевая гипотеза та же - доли пользователей, совершивших определённое действие в объединенной контрольной группе не отличается от долей пользователей, совершивших те же действия в целевой группе.
#немного изменю готовую функтцию
def calc_prop_eq_cons(group1, group2, event):
t1 = events_df_actual.query('group != @group1')['user_id'].nunique()
t2 = events_df_actual.query('group == @group2')['user_id'].nunique()
s1 = events_df_actual.query('group != @group1 and event_name == @event')['user_id'].nunique()
s2 = events_df_actual.query('group == @group2 and event_name == @event')['user_id'].nunique()
p1 = s1 / t1
p2 = s2 / t2
p_comb = (s1 + s2) / (t1 + t2)
z_value = (p1 - p2) / mth.sqrt(p_comb * (1 - p_comb) * (1/t1 + 1/t2))
p_value = (1 - distr.cdf(abs(z_value))) * 2
return p_value, p1, p2
for i in events_wo_tutorial['event_name'].unique():
event_p_val = calc_prop_eq_cons(248, 248, i)
print('Доля пользователей, совершивших событие {0} для группы 246+247: {1}'.format(i, round(event_p_val[1], 2)))
print('Доля пользователей, совершивших событие {0} для группы 248: {1}'.format(i, round(event_p_val[2], 2)))
print('p-значение по событию {0} для групп 246+247 и 248: {1}'.format(i, round(event_p_val[0], 2)))
if (event_p_val[0] < alpha):
print("Отвергаем нулевую гипотезу: между долями есть значимая разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
print('======================================================================================================')
Доля пользователей, совершивших событие MainScreenAppear для группы 246+247: 0.99 Доля пользователей, совершивших событие MainScreenAppear для группы 248: 0.98 p-значение по событию MainScreenAppear для групп 246+247 и 248: 0.29 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие OffersScreenAppear для группы 246+247: 0.61 Доля пользователей, совершивших событие OffersScreenAppear для группы 248: 0.6 p-значение по событию OffersScreenAppear для групп 246+247 и 248: 0.43 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие CartScreenAppear для группы 246+247: 0.5 Доля пользователей, совершивших событие CartScreenAppear для группы 248: 0.48 p-значение по событию CartScreenAppear для групп 246+247 и 248: 0.18 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ====================================================================================================== Доля пользователей, совершивших событие PaymentScreenSuccessful для группы 246+247: 0.47 Доля пользователей, совершивших событие PaymentScreenSuccessful для группы 248: 0.47 p-значение по событию PaymentScreenSuccessful для групп 246+247 и 248: 0.6 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ======================================================================================================
По всем четырем показателям нет оснований считать, что группы между собой отличаются.
Опасения, что новый шрифт отпугнет пользователей, остались опасениями.
Нами исследованы данные о действиях пользователей за неделю наблюдений - с 01.08.2019 по 07.08.2019. За это время 7534 совершили 240887 действий, в среднем 32 действия на пользователя.
Среди 5 видов действий: открытие главного экрана, открытие экрана с предложениями товаров, открытие корзины, появление экрана успешной оплаты и прохождение обучения, 4 участвуют в цепочке, приводящей пользователя к покупке:
Открытие главного экрана → Переход на экран с предложениями товаров → Переход в корзину → Успешная оплата товаров
Каждый второй пользователь, начавший ее, дошел до конца и купил товар. Проблемное место в неё - переход на экран с предложениями товаров, на него пришли только 60 процентов пользователей с предыдущего этапа. Возможно, следует сделать обучение использованию приложения обязательным этапом при его первом открытии или переработать дизайн главной страницы.
Эксперимент по изменению шрифтов для части пользователей прошел безболезненно. Пользователи, в равной степени разделённые на 3 группы, две контрольные и одну целевую, не показали между группами статистически значимых различий между долями пользователей, переходящих по цепочке событий. Шрифты в приложении можно менять без опаски.